﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

#pragma warning disable RS0030
#pragma warning disable CA1305

namespace LinqToDB.Tools.ModelGeneration
{
	/// <summary>
	/// For internal use.
	/// </summary>
	public abstract partial class ModelGenerator
	{
		protected ModelGenerator(
			IModelSource    model,
			StringBuilder   generationEnvironment,
			Action<string?> write,
			Action<string?> writeLine,
			Action<string>  pushIndent,
			Func<string>    popIndent,
			Action<string>  error)
		{
			Model                 = model;
			GenerationEnvironment = generationEnvironment;
			Write                 = write;
			WriteLine             = writeLine;
			PushIndent            = pushIndent;
			PopIndent             = popIndent;
			Error                 = error;

			WriteUsing          = s  => WriteLine($"using {s};");
			WriteComment        = s  => WriteLine($"//{s}");
			WriteBeginNamespace = s  => { WriteLine($"namespace {s}"); WriteLine("{"); };
			WriteEndNamespace   = () => WriteLine("}");
			WriteBeginClass     = WriteBeginClassImpl;
			WriteEndClass       = WriteEndClassImpl;
			WriteAttribute      = WriteAttributeImpl;
			BeginRegion         = s  => WriteLine($"#region {s}");
			EndRegion           = () =>
			{
				Trim();
				WriteLine("");
				WriteLine("#endregion");
			};
			WriteProperty       = WritePropertyImpl;
			WriteField          = WriteFieldImpl;
			WriteEvent          = WriteEventImpl;
			WriteMethod         = WriteMethodImpl;
			GetConstructors     = GetConstructorsImpl;
		}

		public IModelSource    Model                 { get; set; }
		public StringBuilder   GenerationEnvironment { get; }
		public Action<string?> Write                 { get; }
		public Action<string?> WriteLine             { get; }
		public Action<string>  PushIndent            { get; }
		public Func<string>    PopIndent             { get; }
		public Action<string>  Error                 { get; }

		public Action<string>         WriteUsing          { get; set; }
		public Action<string>         WriteComment        { get; set; }
		public Action<string>         WriteBeginNamespace { get; set; }
		public Action                 WriteEndNamespace   { get; set; }
		public Action                 BeforeGenerateModel { get; set; } = () => {};
		public Action<IClass>         WriteBeginClass     { get; set; }
		public Action                 WriteEndClass       { get; set; }
		public Action<IAttribute>     WriteAttribute      { get; set; }
		public Action<string?>        BeginRegion         { get; set; }
		public Action                 EndRegion           { get; set; }
		public Action<IProperty,bool> WriteProperty       { get; set; }
		public Action<IField>         WriteField          { get; set; }
		public Action<IEvent>         WriteEvent          { get; set; }
		public Action<IMethod,bool>   WriteMethod         { get; set; }

		public bool GenerateProcedureErrors { get; set; } = true;

		public static bool EnableNullableReferenceTypes { get; set; }

		public void GenerateModel()
		{
			Model.SetTree();

			if (GenerationEnvironment.Length > 0 && GenerationEnvironment.ToString().Trim().Length == 0)
				GenerationEnvironment.Length = 0;

			WriteComment("---------------------------------------------------------------------------------------------------");
			WriteComment(" <auto-generated>");
			WriteComment("    This code was generated by T4Model template for T4 (https://github.com/linq2db/linq2db).");
			WriteComment("    Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.");
			WriteComment(" </auto-generated>");
			WriteComment("---------------------------------------------------------------------------------------------------");

			WriteLine("");
			WriteLine("#pragma warning disable 1573, 1591");
			if (EnableNullableReferenceTypes)
				WriteLine("#nullable enable");
			WriteLine("");

			BeforeGenerateModel();

			Model.Render(this);
		}

		void WriteAttributeImpl(IAttribute a)
		{
			Write(a.Name);

			if (a.Parameters.Count > 0)
			{
				Write("(");

				for (var i = 0; i < a.Parameters.Count; i++)
				{
					if (i > 0)
						if (a.Parameters[i - 1].All(c => c == ' '))
							Write("  ");
						else
							SkipSpacesAndInsert(", ");
					Write(a.Parameters[i]);
				}

				SkipSpacesAndInsert(")");
			}
		}

		void WriteBeginClassImpl(IClass cl)
		{
			Write(cl.AccessModifier.ToString().ToLower(CultureInfo.InvariantCulture) + " ");

			if (cl.IsStatic)  Write("static ");
			if (cl.IsPartial) Write("partial ");

			Write($"{cl.ClassKeyword} {cl.Name}{(cl.GenericArguments.Count > 0 ? $"<{string.Join(", ", cl.GenericArguments)}>" : string.Empty)}");

			if (!string.IsNullOrEmpty(cl.BaseClass) || cl.Interfaces.Count > 0)
			{
				var arr = new[] { cl.BaseClass }.Concat(cl.Interfaces)
					.Where(n => n != null)
					.ToArray();

				Write(" : ");
				Write(string.Join(", ", arr));
			}

			WriteLine("");
			WriteLine("{");
		}

		void WriteEndClassImpl()
		{
			WriteLine("}");
		}

		void WritePropertyImpl(IProperty p, bool compact)
		{
			var am  = p.AccessModifier == AccessModifier.None ? "" : p.AccessModifier.ToString().ToLower(CultureInfo.InvariantCulture) + " ";
			var mdf = p.IsAbstract ? "abstract " : p.IsVirtual ? "virtual " : p.IsOverride ? "override " : p.IsStatic ? "static " : "";

			Write($"{am}{LenDiff(p.AccessModifierLen, am)}{mdf}{LenDiff(p.ModifierLen, mdf)}{p.BuildType()}{LenDiff(p.TypeLen, p.BuildType()!)} {p.Name}");

			void WriteEndLineComment()
			{
				if (!string.IsNullOrEmpty(p.EndLineComment))
				{
					Write(" ");
					WriteComment(" " + p.EndLineComment);
				}
				else
				{
					WriteLine("");
				}
			}

			if (p.IsAuto)
			{
				Write(LenDiff(p.NameLen + p.ParamLen, p.Name!));

				var len = GenerationEnvironment.Length;

				Write(" { ");

				if (!p.HasGetter)
					Write("private ");
				else if (p.GetterLen == 13)
					Write("        ");
				Write("get; ");

				if (!p.HasSetter)
					Write("private ");
				else if (p.SetterLen == 13)
					Write("        ");
				Write("set; ");

				Write("}");

				if (p.EnforceNotNullable)
					Write(" = null!;");

				if (!string.IsNullOrEmpty(p.EndLineComment))
					WriteSpaces(p.BodyLen - (GenerationEnvironment.Length - len));
				WriteEndLineComment();
			}
			else
			{
				if (compact)
				{
					Write(LenDiff(p.NameLen + p.ParamLen, p.Name!));

					var len = GenerationEnvironment.Length;

					Write(" { ");

					if (p.HasGetter)
					{
						Write("get { ");
						foreach (var t in p.BuildGetBody())
							Write($"{t} ");
						Write("} ");
					}

					if (p.HasSetter)
					{
						Write("set { ");
						foreach (var t in p.BuildSetBody())
							Write($"{t} ");
						Write("} ");
					}

					Write("}");

					if (!string.IsNullOrEmpty(p.EndLineComment))
						WriteSpaces(p.BodyLen - (GenerationEnvironment.Length - len));
					WriteEndLineComment();
				}
				else
				{
					WriteEndLineComment();

					WriteLine("{");
					PushIndent("\t");

					if (p.HasGetter)
					{
						var getBody = p.BuildGetBody().ToArray();
						if (getBody.Length == 1)
						{
							WriteLine($"get {{ {getBody[0]} }}");
						}
						else
						{
							WriteLine("get");
							WriteLine("{");
							PushIndent("\t");

							foreach (var t in getBody)
								WriteLine(t);

							PopIndent();
							WriteLine("}");
						}
					}

					if (p.HasSetter)
					{
						var setBody = p.BuildSetBody().ToArray();
						if (setBody.Length == 1)
						{
							WriteLine($"set {{ {(setBody[0])} }}");
						}
						else
						{
							WriteLine("set");
							WriteLine("{");
							PushIndent("\t");

							foreach (var t in setBody)
								WriteLine(t);

							PopIndent();
							WriteLine("}");
						}
					}

					PopIndent();
					WriteLine("}");
				}
			}
		}

		void WriteFieldImpl(IField f)
		{
			var am = f.AccessModifier.ToString().ToLower(CultureInfo.InvariantCulture);
			var mdf =
				(f.IsStatic   ? " static"   : "") +
				(f.IsReadonly ? " readonly" : "") ;

			Write($"{am}{LenDiff(f.AccessModifierLen, am)}{mdf}{LenDiff(f.ModifierLen, mdf)} {f.BuildType()}{LenDiff(f.TypeLen, f.BuildType()!)} {f.Name}");

			if (f.InitValue != null)
			{
				Write($" = {f.InitValue}");
			}

			Write(";");

			if (!string.IsNullOrEmpty(f.EndLineComment))
			{
				WriteSpaces(f.NameLen - f.Name!.Length + f.BodyLen + f.ParamLen - 1);
				Write(" ");
				WriteComment(" " + f.EndLineComment);
			}
			else
			{
				WriteLine("");
			}
		}

		void WriteEventImpl(IEvent m)
		{
			var am = m.AccessModifier.ToString().ToLower(CultureInfo.InvariantCulture);
			var mdf =
				(m.IsStatic  ? " static"  : "") +
				(m.IsVirtual ? " virtual" : "") +
				" event";

			Write($"{am}{LenDiff(m.AccessModifierLen, am)}{mdf}{LenDiff(m.ModifierLen, mdf)} {m.BuildType()}{LenDiff(m.TypeLen, m.BuildType()!)} {m.Name};");

			if (!string.IsNullOrEmpty(m.EndLineComment))
			{
				WriteSpaces(m.NameLen - m.Name!.Length + m.BodyLen + m.ParamLen - 1);
				Write(" ");
				WriteComment(" " + m.EndLineComment);
			}
			else
			{
				WriteLine("");
			}
		}

		void WriteMethodImpl(IMethod m, bool compact)
		{
			var am1  = m.AccessModifier.ToString().ToLower(CultureInfo.InvariantCulture);
			var len1 = m.AccessModifierLen;
			var am2  = "";
			var len2 = 0;
			var mdf  = m.IsAbstract ? " abstract" : m.IsVirtual ? " virtual" : m.IsOverride ? " override" : m.IsStatic ? " static" : "";
			var mlen = m.ModifierLen;

			if (am1 == "partial" && mdf.Length > 0)
			{
				am2 = " " + am1; len2 = len1 + 1;
				am1 = "";        len1 = 0;
				mdf = mdf.Trim();
				mlen--;
			}

			var gParams = m.GenericArguments.Count > 0 ? $"<{string.Join(", ", m.GenericArguments)}>" : string.Empty;

			Write($"{am1}{LenDiff(len1, am1)}{mdf}{LenDiff(mlen, mdf)}{am2}{LenDiff(len2, am2)}{(m.BuildType() == null ? "" : " ")}{m.BuildType()}{LenDiff(m.TypeLen, m.BuildType() ?? "")} {m.Name}{gParams}");

			void WriteEndLineComment()
			{
				if (!string.IsNullOrEmpty(m.EndLineComment))
				{
					Write(" ");
					WriteComment(" " + m.EndLineComment);
				}
				else
				{
					WriteLine("");
				}
			}

			void WriteParams()
			{
				Write("(");

				for (var i = 0; i < m.ParameterBuilders.Count; i++)
				{
					if (i > 0)
						Write(", ");
					Write(m.ParameterBuilders[i]());
				}

				Write(")");
			}

			if (compact)
			{
				Write(LenDiff(m.NameLen, m.Name!));

				var len = GenerationEnvironment.Length;

				WriteParams();

				foreach (var s in m.AfterSignature)
				{
					Write(" ");
					Write(s);
				}

				len = GenerationEnvironment.Length - len;

				if (m.IsAbstract || m.AccessModifier == AccessModifier.Partial)
				{
					Write(";");
					len = 0;
				}
				else
				{
					WriteSpaces(m.ParamLen - len);

					len = GenerationEnvironment.Length;

					Write(" {");

					foreach (var t in m.BuildBody())
						Write($" {t}");

					Write(" }");
				}

				if (!string.IsNullOrEmpty(m.EndLineComment))
					WriteSpaces(m.BodyLen - (GenerationEnvironment.Length - len));
				WriteEndLineComment();
			}
			else
			{
				WriteParams();
				WriteEndLineComment();

				PushIndent("\t");
				foreach (var s in m.AfterSignature)
					WriteLine(s);
				PopIndent();

				WriteLine("{");
				PushIndent("\t");

				foreach (var t in m.BuildBody())
				{
					if (t.Length > 1 && t[0] == '#')
						RemoveSpace();
					WriteLine(t);
				}

				PopIndent();
				WriteLine("}");
			}
		}

		protected string LenDiff(int max, string str)
		{
			var s = "";
			while (max-- > str.Length)
				s += " ";
			return s;
		}

		public void RemoveSpace()
		{
			Write(" ");

			while (GenerationEnvironment.Length > 0 && GenerationEnvironment[^1] is ' ' or '\t')
				GenerationEnvironment.Length--;
		}

		public void RenderUsings(HashSet<string> usings)
		{
			var q =
				from ns in usings
				group ns by ns.Split('.')[0];

			var groups = q.OrderByDescending(ns => ns.Key == "System").ThenBy(ns => ns.Key);

			foreach (var gr in groups)
			{
				foreach (var ns in from s in gr orderby s select s)
					WriteUsing(ns);
				WriteLine("");
			}

			Trim();
		}

		public void Trim()
		{
			while (GenerationEnvironment.Length > 0 && GenerationEnvironment[^1] is '\r' or '\n' or ' ')
				GenerationEnvironment.Length--;
			WriteLine("");
		}

		public void SkipSpacesAndInsert(string value)
		{
			var l = GenerationEnvironment.Length;

			for (; l > 0 && GenerationEnvironment[l - 1] == ' '; l--)
			{
			}

			GenerationEnvironment.Insert(l, value);
		}

		public IEnumerable<ITree> GetTreeNodes(ITree parent)
		{
			foreach (var node in parent.GetNodes())
			{
				yield return node;
				foreach (var grandNode in GetTreeNodes(node))
					yield return grandNode;
			}
		}

		public void WriteSpaces(int len)
		{
			while (len-- > 0)
				Write(" ");
		}

		public static string ToStringLiteral(string? value)
		{
			if (value == null)
				return "null";

			var sb = new StringBuilder("\"");

			foreach (var chr in value)
			{
				switch (chr)
				{
					case '\t'     : sb.Append("\\t");                  break;
					case '\n'     : sb.Append("\\n");                  break;
					case '\r'     : sb.Append("\\r");                  break;
					case '\\'     : sb.Append("\\\\");                 break;
					case '"'      : sb.Append("\\\"");                 break;
					case '\0'     : sb.Append("\\0");                  break;
					case '\u0085' :
					case '\u2028' :
					case '\u2029' : sb.Append($"\\u{(ushort)chr:X4}"); break;
					default       : sb.Append(chr);                    break;
				}
			}

			sb.Append('"');

			return sb.ToString();
		}

		public string ToCamelCase(string name)
		{
			var n = 0;

			foreach (var c in name)
			{
				if (char.IsUpper(c))
					n++;
				else
					break;
			}

			if (n == 0)
				return name;

			if (n == name.Length)
				return name.ToLower(CultureInfo.InvariantCulture);

			n = Math.Max(1, n - 1);

			return name[..n].ToLower(CultureInfo.InvariantCulture) + name[n..];
		}
	}
}
